iT邦幫忙

2024 iThome 鐵人賽

DAY 15
0

並行編程在現代軟體開發中變得越來越重要,特別是在高效能應用程式中,如何充分利用多核處理器的計算能力至關重要。Rust 作為一個系統程式設計語言,以其獨特的記憶體安全機制和所有權系統,提供了安全且高效的並行程式設計方式。這篇文章將探討 Rust 中的兩種並行編程技術:threadsasync,並展示如何利用這些技術來編寫高效能的並行程式。


一、什麼是並行編程?

並行編程是指讓程式同時執行多個操作或計算的技術。這通常涉及在多個執行緒(threads)上同時運行代碼,以提高性能和資源利用效率。在現代的多核處理器上,並行編程允許應用程式充分利用多個 CPU 核心的計算能力。

Rust 透過兩種主要方式來支持並行編程:

  1. Threads(執行緒):想像你在家裡有兩個人同時在做家事。一個人在打掃客廳,另一個人在洗碗。兩個人各做各的,互不干涉,但每個人都會專心完成自己的任務,直到做完為止。這種方式就是 Threads(執行緒),每個人都是一個執行緒,可以同時進行不同的工作。

  2. Async(非同步):非同步比較像是你一個人在處理多個任務,但是你很會安排事情的先後順序,知道哪些事情可以等、哪些事情需要先做。比如你開始煮飯的時候,煮飯需要時間等待,你就不會乾等著,會利用這段時間去洗碗或摺衣服。等煮飯好了,你再回來看看飯煮得怎麼樣。這就是 Async(非同步),它允許你在等待某件事的時候,去做其他不需要等的事情,讓工作效率提高。


二、Rust 中的 Threads

Rust 的執行緒使用標準函式庫中的 std::thread 模組來創建。Rust 的執行緒模型非常高效,並且透過所有權系統確保了資料的安全共享。

創建一個執行緒

你可以使用 thread::spawn 函數來創建一個新的執行緒。以下是一個簡單的範例,展示如何創建兩個執行緒,並讓它們同時執行不同的操作:

use std::thread; // 引入 thread 模組,用於創建執行緒
use std::time::Duration; // 引入 Duration 模組,用於設定時間長度

fn main() {
    // 創建一個執行緒,並在執行緒中打印訊息
    let handle = thread::spawn(|| {
        for i in 1..5 { // 執行緒中的迴圈,從 1 執行到 4
            println!("執行緒 1:數字 {} ", i); // 打印執行緒中的數字
            thread::sleep(Duration::from_millis(500)); // 暫停 500 毫秒
        }
    });

    // 主執行緒同時執行
    for i in 1..3 { // 主執行緒中的迴圈,從 1 執行到 2
        println!("主執行緒:數字 {} ", i); // 打印主執行緒中的數字
        thread::sleep(Duration::from_millis(500)); // 暫停 500 毫秒
    }

    // 等待執行緒完成
    handle.join().unwrap(); // 等待子執行緒執行完畢後再繼續
    println!("全部執行完畢");
}

結果:

主執行緒:數字 1 
執行緒 1:數字 1 
主執行緒:數字 2 
執行緒 1:數字 2 
執行緒 1:數字 3 
執行緒 1:數字 4 
全部執行完畢
  • handle.join() 是用來確保主執行緒等待新執行緒完成的方式。如果你不使用 join,主執行緒可能在新執行緒完成之前就結束了,導致你無法看到新執行緒的輸出。
  • Duration::from_millis() 是 Rust 標準庫中的一個方法,用於創建一個 Duration 物件,代表以毫秒為單位的時間長度。這個方法在 Rust 的並行和異步編程中經常用來設定等待時間或計時。

三、執行緒中的資料共享

在多執行緒的程式設計中,讓多個執行緒能夠共用資料是很常見的需求,但這樣做也可能會引發「搶著使用」的問題,導致資料不一致或錯誤。Rust 使用它特有的所有權系統和安全的並行編程工具,來幫助我們解決這些問題,確保多個執行緒在共享資料時是安全的、不會出錯。

使用 ArcMutex 共享資料

如果你想在多個執行緒間共享資料,通常會使用 Arc(原子引用計數)和 Mutex(互斥鎖)來實現安全的資料共享。以下範例展示了如何使用 ArcMutex 在多個執行緒間共享一個可變變數。

use std::sync::{Arc, Mutex}; // 引入 Arc 和 Mutex,用於安全共享資料
use std::thread; // 引入 thread 模組,用於創建執行緒

fn main() {
    // 創建一個共享的計數器,使用 Arc 和 Mutex 進行保護
    let counter = Arc::new(Mutex::new(0)); 
    let mut handles = vec![]; // 創建一個空的向量來存儲執行緒的 handle

    // 創建 10 個執行緒,每個執行緒都會增加計數器
    for _ in 0..10 {
        let counter = Arc::clone(&counter); // 克隆 Arc,讓每個執行緒都能共享同一個計數器
        let handle = thread::spawn(move || { // 創建一個新執行緒
            let mut num = counter.lock().unwrap(); // 獲取 Mutex 的鎖,並安全地訪問計數器
            *num += 1; // 增加計數器的值
        });
        handles.push(handle); // 將執行緒的 handle 添加到向量中
    }

    // 等待所有執行緒完成
    for handle in handles {
        handle.join().unwrap(); // 等待每個執行緒完成執行
    }

    // 顯示最終的計數值
    println!("最終計數器:{}", *counter.lock().unwrap()); // 獲取 Mutex 的鎖,並打印計數器的值
}

結果:

最終計數器:10

Arc(Atomic Reference Counted)

  • 功能Arc 是一個智能指標,允許多個執行緒安全地共享相同的資料。它使用原子操作來管理內部的引用計數,確保同一時間可以安全地被多個執行緒引用。
  • 原理Arc 透過增加和減少引用計數來管理內存的所有權。當引用計數變為 0 時,資料會被釋放。
  • 使用場合:適合多執行緒需要共享讀取同一資料的情況,但資料是不可變的,或者需要結合 Mutex 來實現可變的共享。

Mutex(Mutual Exclusion)

  • 功能Mutex 是一種互斥鎖,用來保護共享的可變資料,確保同一時間只有一個執行緒能夠訪問這些資料。
  • 原理:在執行緒進入臨界區(critical section)時,必須先獲得 Mutex 的鎖。當一個執行緒獲得鎖後,其他執行緒將被阻塞,直到鎖被釋放。
  • 使用場合:適合在多執行緒需要讀寫同一資料的情況下使用,保證資料的一致性和安全性。

範例說明:

在下面的範例中,我們創建了一個 Arc<Mutex<i32>>,這樣做的目的是讓多個執行緒可以安全地共享一個計數器,並確保每次訪問和修改計數器時,只有一個執行緒能夠進行操作。

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 創建一個共享的計數器,使用 Arc 和 Mutex 保護
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    // 創建 10 個執行緒,每個執行緒都會增加計數器
    for _ in 0..10 {
        let counter = Arc::clone(&counter); // 克隆 Arc 以便執行緒共享
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap(); // 獲取 Mutex 鎖,安全訪問數據
            *num += 1; // 增加計數器
        });
        handles.push(handle);
    }

    // 等待所有執行緒完成
    for handle in handles {
        handle.join().unwrap();
    }

    // 顯示最終的計數值
    println!("最終計數器:{}", *counter.lock().unwrap());
}

在 Python 中的類似概念:ThreadingLock

在 Python 中,類似的功能可以通過 threading 模組中的 Lock 來實現。以下是 Python 版本的程式碼,用於演示如何在多執行緒之間安全地共享資料:

import threading

# 創建一個全域變數計數器和一個鎖
counter = 0
counter_lock = threading.Lock()

def increment_counter():
    global counter
    with counter_lock:  # 獲取鎖
        counter += 1  # 安全地增加計數器

threads = []

# 創建 10 個執行緒
for _ in range(10):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

# 等待所有執行緒完成
for thread in threads:
    thread.join()

print(f"最終計數器:{counter}")

比較:

  • 共享資料方式:Rust 使用 Arc 來共享資料,而 Python 使用全域變數或物件。
  • 保護機制:Rust 使用 Mutex 確保安全訪問,而 Python 使用 Lock 進行互斥控制。
  • 錯誤處理:Rust 的 Mutex 會強制處理錯誤(例如解鎖失敗時),而 Python 的 Lock 相對簡單,但對於資料一致性和安全性的控制較弱。
  • 效能:Rust 的 Mutex 在效能上通常優於 Python 的 Lock,特別是在高並發環境中,由於 Rust 更加接近底層和無 GIL 限制。

四、Rust 中的 Async

Rust 的異步編程模型主要基於 Futureasync/await,這讓 Rust 能夠非常高效地處理需要等待的操作,而不必浪費執行緒的資源。當程式遇到需要等待的事情(例如網路請求、文件讀取等),它可以選擇繼續做其他任務,直到前面的操作完成時再回來。這讓程式運作更加靈活和有效率。

基本的 async 語法

要使用 Rust 的非同步功能,可以用 async 關鍵字來定義非同步函數,並用 await 來等待這些函數完成。下面的範例展示了如何在 Rust 中運行非同步任務,並對其行為進行更直觀的解釋。

  1. 首先引入 tokio 套件
    Cargo.toml 檔案中加入以下內容來安裝 tokio,這是 Rust 中常用的非同步運行時:

    [dependencies]
    tokio = { version = "1", features = ["full"] }
    

    設定完成後,記得執行 cargo build 來安裝這個套件。

  2. 非同步程式碼範例說明

    use std::time::Duration;
    use tokio::time::sleep;
    
    // 定義一個非同步任務 async_task
    async fn async_task(id: u32) {
        println!("非同步任務 {} 開始", id); // 顯示任務開始
        sleep(Duration::from_secs(2)).await; // 非同步等待 2 秒
        println!("非同步任務 {} 完成", id); // 顯示任務完成
    }
    
    #[tokio::main]
    async fn main() {
        println!("呼叫非同步任務");
    
        // 創建多個非同步任務並同時執行
        let task1 = async_task(1);
        let task2 = async_task(2);
    
        // 使用 tokio::join! 同時運行兩個非同步任務
        tokio::join!(task1, task2);
    
        println!("所有非同步任務已完成");
    
        // 這部分是主執行緒的同步操作
        for i in 1..=3 {
            println!("主執行緒的同步操作 {}", i);
            tokio::time::sleep(Duration::from_millis(500)).await; // 模擬主執行緒的操作
        }
    }
    

逐步解釋這段程式碼

  1. 定義非同步任務 (async_task)

    • async fn async_task(id: u32):定義了一個非同步函數,該函數等待 2 秒後顯示任務完成。
    • sleep(Duration::from_secs(2)).await:使用 await 等待操作完成,這期間可以讓其他非同步任務繼續執行。
  2. 執行非同步任務

    • #[tokio::main]:這個標籤告訴 Rust 使用 tokio 來運行這個非同步的 main 函數。
    • tokio::join!(task1, task2):同時執行 task1task2,並等待這兩個任務完成,然後繼續下一步操作。
  3. 主執行緒的同步操作

    • 在非同步任務結束後,才執行主執行緒的同步操作。這部分展示了即使有非同步任務進行中,程式依然會按照執行順序來處理。

結果分析

實際輸出顯示非同步任務是先完成,然後再進行主執行緒的同步操作。這證明了 tokio::join! 是等待所有非同步任務結束後才進行下一步的操作:

呼叫非同步任務
非同步任務 1 開始
非同步任務 2 開始
非同步任務 2 完成
非同步任務 1 完成
所有非同步任務已完成
主執行緒的同步操作 1
主執行緒的同步操作 2
主執行緒的同步操作 3

這段程式碼展示了非同步任務如何同時執行,提升了程式在處理多個操作時的效率。實際輸出結果顯示,非同步任務在幾乎同一時間開始並且互不干涉,直到所有任務完成後,程式才會繼續進行後續的同步操作。

從結果可以看到,tokio::join! 讓兩個非同步任務同時開始執行,並等到它們都完成後,才會繼續執行主執行緒的操作,通常這樣的設計是用於需要等待外部資源或者特定函數程序處理完畢之後才能繼續下一步的情境。


五、何時使用 threadsasync

  • Threads:適合 CPU 密集型任務或需要並行處理的場景,例如大量計算或處理大型資料集。執行緒是並行運行的,適合多核 CPU 的應用。

  • Async:適合 IO 密集型任務,例如網路請求、文件讀寫等,這些任務通常會等待外部資源完成。非同步編程允許單個執行緒處理多個任務,而不需要創建大量執行緒。


六、Python 與 Rust 的 Thread 效能比較

在這裡,我們透過一個有趣的小測試來比較 Python 和 Rust 在多執行緒處理上的效能表現。測試的內容非常簡單:我們讓兩種語言各自進行大量的計算任務——找出 50,000 以內的素數。素數計算是一種典型的 CPU 密集型工作,適合用來測試多執行緒的效能。

Python 範例:多線程 vs. 多進程

Python 的範例使用了兩種方法:多線程多進程。在 Python 中,由於 GIL(全域直譯器鎖)的限制,多線程在 CPU 密集型任務中不能完全發揮效能,所以我們也使用 multiprocessing 來創建多個獨立的進程,看看它是否會更快一些。

import threading  # 引入 threading 模組,用於創建和操作執行緒
import time  # 引入 time 模組,用於計算執行時間
from multiprocessing import Pool  # 引入 multiprocessing 模組中的 Pool,用於多進程處理

# 定義一個計算素數的函數
def count_primes(n):
    count = 0  # 計數器,初始化為 0
    for i in range(2, n):  # 從 2 開始迭代到 n-1
        for j in range(2, int(i ** 0.5) + 1):  # 檢查從 2 到該數字平方根的所有數
            if i % j == 0:  # 如果能整除,則不是素數
                break
        else:
            count += 1  # 如果沒被 break 中斷,則是素數,計數器加 1
    return count  # 返回素數的個數

# 定義一個執行緒的函數
def run_threaded():
    count_primes(50000)  # 計算 50000 以內的素數

if __name__ == '__main__':
    # 使用 threading 的範例
    threads = [threading.Thread(target=run_threaded) for _ in range(2)]  # 創建兩個執行緒,每個執行 run_threaded

    start_time = time.time()  # 記錄開始時間

    # 啟動執行緒
    for thread in threads:
        thread.start()  # 啟動每個執行緒

    # 等待執行緒完成
    for thread in threads:
        thread.join()  # 等待每個執行緒結束

    elapsed_time = (time.time() - start_time) * 1000  # 計算執行時間,並轉換為毫秒
    print(f"Threading - 執行時間:{elapsed_time:.2f} 毫秒")  # 打印 threading 的執行時間

    # 使用 multiprocessing 的範例
    start_time = time.time()  # 記錄開始時間

    with Pool(2) as pool:  # 創建兩個進程的池
        pool.map(count_primes, [50000, 50000])  # 並行計算兩次 count_primes,參數各為 50000

    elapsed_time = (time.time() - start_time) * 1000  # 計算執行時間,並轉換為毫秒
    print(f"Multiprocessing - 執行時間:{elapsed_time:.2f} 毫秒")  # 打印 multiprocessing 的執行時間

Rust 範例:多執行緒的效能測試

接著,我們來看 Rust 的版本。Rust 使用 std::thread 來進行多執行緒運算,由於沒有 GIL 的限制,Rust 的執行緒可以完全並行地工作,充分利用多核 CPU 的威力。

use std::thread;
use std::time::Instant;

// 計算 50,000 以內的素數
fn count_primes(n: u64) -> u64 {
    let mut count = 0;
    for i in 2..n {
        if (2..=((i as f64).sqrt() as u64)).all(|j| i % j != 0) {
            count += 1;
        }
    }
    count
}

fn main() {
    let start_time = Instant::now();

    // 創建兩個執行緒,並行計算
    let handles: Vec<_> = (0..2)
        .map(|_| {
            thread::spawn(|| {
                count_primes(50000);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Rust - 執行時間:{:.2?} 毫秒", start_time.elapsed());
}

效能比較結果

執行這些程式後,我們得到以下結果:

  • Python 的執行結果

    Threading - 執行時間:89.66 毫秒
    Multiprocessing - 執行時間:207.09 毫秒
    

    可以看到,Python 的多線程效能雖然不錯,但仍受到 GIL 的影響,無法完全釋放 CPU 的力量;而多進程雖然繞過了 GIL,但建立和管理進程的成本較高,因此耗時更多。

  • Rust 的執行結果

    Rust - 執行時間:14.84 毫秒
    

    Rust 的表現非常驚人,運行速度大幅超越 Python,這主要歸功於 Rust 的多執行緒可以真正並行運行,而不會受到 GIL 限制。Rust 的編譯器優化和系統級效能,使得這類 CPU 密集型工作如魚得水。

總結

對於習慣使用 Python 的開發者來說,當面對大量資料處理或高效能需求時,經常會感受到 Python 的不足。即使使用多線程或多進程技術提升效能,仍難以突破 GIL 的瓶頸。但透過這篇文章的比較,我們可以清楚看到 Rust 在多執行緒應用上的顯著優勢。

這不僅僅是效能的提升,更代表著開發潛力和用戶體驗的進一步突破。Rust 的記憶體安全性、所有權系統、以及無與倫比的執行效能,讓它成為高效能應用程式開發的強力選擇。

看到這些成果,相信你和我一樣感到興奮!接下來的日子,我們將繼續探索 Rust 為 Python 開發者帶來的更多驚喜——15 天過去了,Rust還有哪些值得Python開發者們探索的地方呢?就讓我們繼續看下去~


上一篇
[Day 14] Rust 中的模式匹配:match 的進階應用
下一篇
[Day 16] 泛型與特徵物件:提升 Rust 代碼的彈性與重用性
系列文
從 Python 開發者的角度學習 Rust —— 從語法基礎到實戰應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言